{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 错误、调试和测试"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1 错误处理"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. 使用错误码来表示出错，但不方便\n",
    "2. 使用异常处理机制`try...except...finally`，捕获异常并处理"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. try "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.正常运行的结果:\n",
      "try...\n",
      "result: 5.0\n",
      "No Error!\n",
      "finally...\n",
      "END\n",
      "2.捕获异常后的结果:\n",
      "try...\n",
      "ZeroDivisionError: division by zero\n",
      "finally...\n",
      "END\n",
      "3.捕获异常后的结果:\n",
      "try...\n",
      "ValueError: invalid literal for int() with base 10: 'a'\n",
      "finally...\n",
      "END\n"
     ]
    }
   ],
   "source": [
    "# 异常捕获的例子\n",
    "def run(num):\n",
    "    try:                               # try语句块中放运行过程可能出现异常的句子\n",
    "        print('try...')\n",
    "        r = 10 / int(num)\n",
    "        print('result:', r)\n",
    "    except ValueError as err:          # try语句块运行时出现异常后，跳转到该语句块，否则直接跳过该块\n",
    "        print('ValueError:', err)\n",
    "    except ZeroDivisionError as err:   # 可以有多个except来捕获不同的错误类型\n",
    "        print('ZeroDivisionError:', err)\n",
    "    else:                                 \n",
    "        print('No Error!')             # 如果没有出现异常，会执行else语句块中内容\n",
    "    finally:                           # 无论是否异常，finally语句块都会执行\n",
    "        print('finally...')\n",
    "\n",
    "    print('END')\n",
    "print('1.正常运行的结果:')\n",
    "run('2')\n",
    "print('2.捕获异常后的结果:')\n",
    "run(0)\n",
    "print('3.捕获异常后的结果:')\n",
    "run('a')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Error: division by zero\n",
      "finally...\n"
     ]
    }
   ],
   "source": [
    "# 异常捕捉的跨层调用实例\n",
    "def foo(s):\n",
    "    return 10 / int(s)\n",
    "def bar(s):\n",
    "    return foo(s) * 2\n",
    "def main():\n",
    "    try:\n",
    "        print('result:', bar(0))\n",
    "    except Exception as err:   # 如果不知道捕获什么错误类型，可以直接使用Exception\n",
    "        print('Error:', err)\n",
    "    finally:\n",
    "        print('finally...')\n",
    "main()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "小结:\n",
    "- python错误也是类Class\n",
    "- except可以捕捉错误类型及它的子类错误\n",
    "- try...except可以跨层调用\n",
    "- 如果不明确捕获的错误类型，可以直接使用`except Exception as err:`能捕获各类异常\n",
    "- 使用调用栈返回的错误信息，定位错误源后进行debug"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. 记录错误\n",
    "捕获错误的同时记录下来，并使程序可以继续运行，使用`logging`模块记录错误信息"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "ERROR:root:division by zero\n",
      "Traceback (most recent call last):\n",
      "  File \"<ipython-input-3-42c6dc21d823>\", line 9, in main\n",
      "    print('result:', bar(0))\n",
      "  File \"<ipython-input-3-42c6dc21d823>\", line 6, in bar\n",
      "    return foo(s) * 2\n",
      "  File \"<ipython-input-3-42c6dc21d823>\", line 4, in foo\n",
      "    return 10 / int(s)\n",
      "ZeroDivisionError: division by zero\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "finally...\n"
     ]
    }
   ],
   "source": [
    "# 使用logging模块记录错误信息\n",
    "import logging\n",
    "def foo(s):\n",
    "    return 10 / int(s)\n",
    "def bar(s):\n",
    "    return foo(s) * 2\n",
    "def main():\n",
    "    try:\n",
    "        print('result:', bar(0))\n",
    "    except Exception as err:      # 如果不知道捕获什么错误类型，可以直接使用Exception\n",
    "        logging.exception(err)    # logging记录错误信息\n",
    "    finally:\n",
    "        print('finally...')\n",
    "        \n",
    "main()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. 抛出错误\n",
    "`raise ValueError('描述信息')`可以抛出异常"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 1. 自定义错误类型\n",
    "class FooError(ValueError):\n",
    "    pass\n",
    "def foo(s):\n",
    "    n = int(s)\n",
    "    if n == 0:\n",
    "        raise FooError('invalid value: %s' % s)\n",
    "    return 10 / n\n",
    "# foo('0')     # 会抛出异常 FooError: invalid value: 0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "一般默认的错误类型已经够用，所以不必自定义错误类型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 2. 捕获异常后再次抛出异常，不断向上抛出异常，便于使用调用栈的错误信息定位错误源\n",
    "# 错误信息：记录一下，便于后续追踪\n",
    "def foo(s):\n",
    "    n = int(s)\n",
    "    if n == 0:\n",
    "        raise ValueError('invalid value: %s' % s)\n",
    "    return 10 / n\n",
    "def bar():\n",
    "    try:\n",
    "        foo('0')\n",
    "    except ValueError as err:\n",
    "        print('ValueError')\n",
    "        raise\n",
    "# bar()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "小结:\n",
    "- raise不带参数，会把错误原样抛出\n",
    "- except中加raise，可以将错误类型转换"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "异常捕捉练习"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "100 + 200 + 345 = 645.0\n",
      "99 + 88 + 7.6 = 194.6\n"
     ]
    }
   ],
   "source": [
    "# 根据错误信息，定位错误源，并修改\n",
    "from functools import reduce\n",
    "\n",
    "def str2num(s):\n",
    "#     return int(s)\n",
    "    return float(s)        # 或 float(s)都行\n",
    "\n",
    "def calc(exp):\n",
    "    ss = exp.split('+')\n",
    "    ns = map(str2num, ss)\n",
    "    return reduce(lambda acc, x: acc + x, ns)\n",
    "\n",
    "def main():\n",
    "    r = calc('100 + 200 + 345')\n",
    "    print('100 + 200 + 345 =', r)\n",
    "    r = calc('99 + 88 + 7.6')\n",
    "    print('99 + 88 + 7.6 =', r)\n",
    "\n",
    "main()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2 调试"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. 使用print()函数打印中间变量\n",
    "- 优点：简单、明了可以查看程序的运行中间情况\n",
    "\n",
    "- 缺点：需要将不使用的print()函数注释掉，否则会输出太多信息"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def foo(s):\n",
    "    n = int(s)\n",
    "    print('>>> n = %d' % n)\n",
    "    return 10 / n\n",
    "\n",
    "def main():\n",
    "    foo('0')\n",
    "\n",
    "# main()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. 断言assert\n",
    "可以判断某个变量是否满足要求，并且可以使用`-O`参数关掉断言功能：`python -O file_name.py`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "def foo(s):\n",
    "    n = int(s)\n",
    "    assert n != 0, 'n is zero!'    # 如果n!=0不成立，则抛出异常，所以assert可以保证某个参数满足要求\n",
    "    return 10 / n\n",
    "def main():\n",
    "    foo('0')\n",
    "# main()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. logging模块\n",
    "可以替换print()函数，不会抛出异常，并且可以输出到文件中"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "# logging模块可以输出信息\n",
    "import logging\n",
    "logging.basicConfig(level=logging.INFO)  # 控制输出的级别，有debug、info、warning和error\n",
    "def foo(s):\n",
    "    n = int(s)\n",
    "    logging.info('n= %d', n)   # info相当于print函数\n",
    "    return 10 / n\n",
    "def main():\n",
    "    foo('7')\n",
    "main()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "ERROR:root:This is a error message.\n",
      "WARNING:root:This is a warning message.\n"
     ]
    }
   ],
   "source": [
    "# logging控制输出信息的级别\n",
    "import logging\n",
    "# logging.basicConfig(filename= __name__ + '.log', level=logging.DEBUG, filemode='a')\n",
    "logging.basicConfig(filename='LOG/' + __name__+'.log', \\\n",
    "                    format='[%(asctime)s-%(filename)s-%(levelname)s:%(message)s]', \\\n",
    "                    level = logging.DEBUG, \\\n",
    "                    filemode='a', \\\n",
    "                    datefmt='%Y-%m-%d%I:%M:%S %p')\n",
    "def test():\n",
    "    logging.error('This is a error message.')\n",
    "    logging.warning('This is a warning message.')\n",
    "    logging.info('This is a info message.')\n",
    "    logging.debug('This is a debug message.')\n",
    "    logging.info('-----------------------------------')\n",
    "test()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "小结:\n",
    "\n",
    "1. Log系统有6个级别，优先级(数越大，优先级越高)\n",
    "\n",
    "|||\n",
    "|-|-|-|-|-|\n",
    "|类型|CRITICAL|ERROR|WARNING|INFO|DEBUG|NOTSET\n",
    "|优先级|50|40|30|20|10|0|\n",
    "2. basicConfig的设置\n",
    "\n",
    "\n",
    "- Filename：指定路径的文件。这里使用了+`—name—`+是将log命名为当前py的文件名\n",
    "\n",
    "- Format：设置log的显示格式（即在文档中看到的格式）。分别是时间+当前文件名+log输出级别+输出的信息\n",
    "\n",
    "- Level：输出的log级别，优先级比设置的级别低的将不会被输出保存到log文档中\n",
    "\n",
    "- Filemode：log打开模式。\n",
    "a：代表每次运行程序都继续写log。即不覆盖之前保存的log信息。\n",
    "w：代表每次运行程序都重新写log。即覆盖之前保存的log信息"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  pdb，IDE都是设置断点的形式来调试程序"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3 单元测试"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 1. 待测试部分，自定义一个字典\n",
    "class Dict(dict):\n",
    "    def __init__(self, **kw):    # 子类的初始化函数\n",
    "        super().__init__(**kw)   # 继承中使用super代表使用父类的方法，使用父类的初始化\n",
    "    def __getattr__(self, key):  # 获取字典的key对应的value\n",
    "        try:\n",
    "            return self[key]\n",
    "        except KeyError:         # 如果没有该key，则出现KeyError错误\n",
    "            raise AttributeError(r\"'Dict' object has no attribute '%s'\" % key)\n",
    "    def __setattr__(self, key, value):\n",
    "        self[key] = value"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 2. 单元测试部分，拿所有可能结果的实例来测试，包括抛出异常\n",
    "import unittest\n",
    "class TestDict(unittest.TestCase):            # 继承后的子类\n",
    "    def test_init(self):                      # 初始化检测\n",
    "        d = Dict(a=1, b='test')\n",
    "        self.assertEqual(d.a, 1)              # 相当于判断 d.a == 1 \n",
    "        self.assertEqual(d.b, 'test')\n",
    "        self.assertTrue(isinstance(d, dict))  # 相当于判断 d的类型是dict\n",
    "    def test_key(self):                       # 测试key值赋值value\n",
    "        d = Dict()\n",
    "        d['key'] = 'value'                    # 为什么加单引号？？？？？\n",
    "        self.assertEqual(d['key'], 'value')\n",
    "    def test_attr(self):\n",
    "        d = Dict()\n",
    "        d.key = 'value'                      # ???为什么可以这样赋值\n",
    "        self.assertTrue('key' in d)\n",
    "        self.assertEqual(d['key'], 'value')\n",
    "    def test_keyerror(self):\n",
    "        d = Dict()\n",
    "        with self.assertRaises(KeyError):\n",
    "            value = d['empty']               # ???\n",
    "    def test_attrerror(self):\n",
    "        d = Dict()\n",
    "        with self.assertRaises(AttributeError):\n",
    "            value = d.empty\n",
    "    def setUp(self):                        # 每调用一个测试方法前执行\n",
    "        print('setUp...')\n",
    "\n",
    "    def tearDown(self):                     # 每调用一个测试方法后执行\n",
    "        print('tearDown...')\n",
    "       \n",
    "# if __name__ == '__main__':   # 在jupyter环境下，进行模块测试会出错\n",
    "#     unittest.main()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      ".....\n",
      "----------------------------------------------------------------------\n",
      "Ran 5 tests in 0.001s\n",
      "\n",
      "OK\n"
     ]
    }
   ],
   "source": [
    "# !python mydict_test.py   # 该句必须在文件中加if __name__ == '__main__'的部分\n",
    "# 测试程序写在文件里了\n",
    "!python  test_unit_document/mydict_test.py\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "3\n",
      "4\n"
     ]
    }
   ],
   "source": [
    "from  test_unit_document.mydict import Dict\n",
    "dic = Dict(a=1, b='2',c = 2)\n",
    "dic['k1'] = 3\n",
    "print(dic['k1'])\n",
    "dic.k2 = 4\n",
    "print(dic.k2)\n",
    "# print(dic[3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "小结:\n",
    "- 一般模块名都是小写字母，如`unittest, numpy`等模块，一般类名称为驼峰命名，首字母大写，或最小词义各部分首字母大写，如`Dict, MyDict, TestDict`，方法及属性一般都是小写字母\n",
    "- 测试单元的测试用例覆盖常用输入组合、边界条件和异常"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 文档测试"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 在注释中加入测试的示例，使用doctest模块测试示例，如果没有错误不会输出错误\n",
    "!python  test_unit_document/mydict2.py"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
